/*
   Copyright (c) 2023 李伟国
   learner-platform is licensed under Mulan PSL v2.
   You can use this software according to the terms and conditions of the Mulan PSL v2.
   You may obtain a copy of Mulan PSL v2 at:
               http://license.coscl.org.cn/MulanPSL2
   THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  
   See the Mulan PSL v2 for more details. 
*/

package cn.platform.shiro.jwt;

import cn.platform.constant.RedisConstant;
import cn.platform.utils.JWTUtil;
import cn.platform.utils.RedisUtil;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 执行过程：
 *
 * preHandle==>isAccessAllowed==>isLoginAttempt==>executeLogin
 */
public class JWTFilter extends BasicHttpAuthenticationFilter {
    private static final Logger log = LoggerFactory.getLogger(JWTFilter.class);

    /**
     * 如果带有 token，则对 token 进行检查，否则直接通过。
     * 如果请求头不存在 Token，则可能是执行登陆操作或者是游客状态访问，无需检查 token，直接返回 true
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        log.info("==== isAccessAllowed ====");
        if(isLoginAttempt(request, response)){
            try{
                executeLogin(request,response);
                return true;
            }catch (Exception e){
                log.info("=== 刷新token ===");
                Throwable cause = e.getCause();
                if(e instanceof TokenExpiredException){
                    String result = refreshToken(request, response);
                    if(result.equals("success")){
                        log.info("===  成功刷新token  ===");
                        return true;
                    }
                }
                throw new UnauthenticatedException();
            }
        }
        return true;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
//        return super.isLoginAttempt(request, response);
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(RedisConstant.TOKEN_HEADER_LOGIN.getValue());
        return token!=null;
    }
    /*
     * executeLogin实际上就是先调用createToken来获取token，这里我们重写了这个方法，就不会自动去调用createToken来获取token
     * 然后调用getSubject方法来获取当前用户再调用login方法来实现登录
     * 这也解释了我们为什么要自定义jwtToken，因为我们不再使用Shiro默认的UsernamePasswordToken了。
     * */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//        return super.executeLogin(request, response);
        log.info("===  executeLogin  ===");
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(RedisConstant.TOKEN_HEADER_LOGIN.getValue());
        JWTToken jwt = new JWTToken(token);
        getSubject(request,response).login(jwt);
        return true;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return super.preHandle(request, response);
    }

    /*
     * 这里的getBean是因为使用@Autowired无法把RedisUtil注入进来
     * 这样自动去注入当使用的时候是未NULL，是注入不进去了。通俗的来讲是因为拦截器在spring扫描bean之前加载所以注入不进去。
     *
     * 解决的方法：
     * 可以通过已经初始化之后applicationContext容器中去获取需要的bean.
     * */
    public <T> T getBean(Class<T> clazz, HttpServletRequest request){
        WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        return applicationContext.getBean(clazz);
    }

    // 刷新token
    private String refreshToken(ServletRequest request,ServletResponse response) {
        log.info("=== refreshToken ===");
        HttpServletRequest req = (HttpServletRequest) request;
        RedisUtil redisUtil = getBean(RedisUtil.class, req);
        String accessToken = req.getHeader(RedisConstant.TOKEN_HEADER_LOGIN.getValue());
        String username = JWTUtil.getUsername(accessToken);
        Long userId = JWTUtil.getUserId(accessToken);
        String salt = JWTUtil.getSalt(accessToken);
        log.info("=== 用户名 {} ===",username);
        log.info("redisUtil.hasKey({}) {}",username,redisUtil.hasKey(username));
        if(redisUtil.hasKey(username)){
            Long current = (Long)redisUtil.get(username);
            if(current.equals(JWTUtil.getExpire(accessToken))){
                long currentTimeMillis = System.currentTimeMillis();
                String token = JWTUtil.createToken(salt,username, userId, currentTimeMillis);
                redisUtil.set(username,currentTimeMillis,30*60);
                JWTToken jwtToken = new JWTToken(token);
                getSubject(request,response).login(jwtToken);
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader(RedisConstant.TOKEN_HEADER_LOGIN.getValue(), token);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", RedisConstant.TOKEN_HEADER_LOGIN.getValue());
                return "success";
            }
        }
        return "token认证失败，token过期，重新登录";
    }
}
